﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

using FirstFloor.Documents.IO;

namespace FirstFloor.Documents.Pdf
{
    /// <summary>
    /// Reads tokens from a stream
    /// </summary>
    internal class PdfLexer
        : IDisposable
    {
        private const char Comment = '%';

        private static char[] Whitespace = new char[] { '\x00', '\x09', '\x0a', '\x0c', '\x0d', '\x20' };
        private static char[] Delimiters = new char[] { '(', ')', '[', ']', '<', '>', '{', '}', '/', Comment };

        private List<Token> tokens = new List<Token>();
        private bool disposed;

        /// <summary>
        /// Initializes a new instance of the <see cref="PdfLexer"/> class.
        /// </summary>
        /// <param name="stream">The stream.</param>
        public PdfLexer(Stream stream)
        {
            stream.VerifyReadAndSeek();
            this.Stream = stream;
        }

        /// <summary>
        /// Gets the stream.
        /// </summary>
        /// <value>The stream.</value>
        public Stream Stream { get; private set; }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (!this.disposed) {
                this.disposed = true;
                this.Stream.Dispose();
            }
        }

        /// <summary>
        /// Clears the token and character buffers.
        /// </summary>
        public void ClearBuffers()
        {
            this.tokens.Clear();
        }

        /// <summary>
        /// Reads a fixed number of bytes from the stream.
        /// </summary>
        /// <param name="length">The length.</param>
        /// <returns></returns>
        public byte[] ReadBytes(int length)
        {
            ClearBuffers();
            return this.Stream.ReadBytes(length);
        }

        /// <summary>
        /// Sets the position within the current stream.
        /// </summary>
        /// <param name="offset">The offset.</param>
        /// <param name="origin">The origin.</param>
        public void Seek(long offset, SeekOrigin origin)
        {
            ClearBuffers();
            this.Stream.Seek(offset, origin);
        }

        /// <summary>
        /// Determines whether a token exists in the stream.
        /// </summary>
        /// <returns>
        /// 	<c>true</c> if [has next token]; otherwise, <c>false</c>.
        /// </returns>
        public bool HasNextToken()
        {
            if (this.tokens.Count == 0) {
                var token = ReadToken(false);
                if (token == null) {
                    return false;
                }
                
                this.tokens.Add(token.Value);
            }
            return true;
        }

        /// <summary>
        /// Reads the next token without consuming it.
        /// </summary>
        /// <returns></returns>
        public Token Peek()
        {
            return Peek(0);
        }

        /// <summary>
        /// Reads the specified token without consuming it.
        /// </summary>
        /// <param name="lookAhead">The look ahead.</param>
        /// <returns></returns>
        public Token Peek(int lookAhead)
        {
            if (lookAhead < 0 || lookAhead > 9) {
                throw new ArgumentOutOfRangeException();
            }

            while (lookAhead > this.tokens.Count - 1) {
                this.tokens.Add(ReadToken(true).Value);
            }
            return this.tokens[lookAhead];
        }

        /// <summary>
        /// Consumes the next token.
        /// </summary>
        /// <returns></returns>
        public Token Read()
        {
            if (this.tokens.Count > 0) {
                var token = tokens[0];
                this.tokens.RemoveAt(0);
                return token;
            }
            else {
                return ReadToken(true).Value;
            }
        }

        /// <summary>
        /// Consumes the next token that must match specified type.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        public Token Read(TokenType type)
        {
            return Read().Match(type);
        }

        /// <summary>
        /// Consumes the next token that must match specified type and value.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="value">The value.</param>
        /// <returns></returns>
        public Token Read(TokenType type, string value)
        {
            return Read().Match(type, value);
        }

        /// <summary>
        /// Reads any whitespace.
        /// </summary>
        /// <returns></returns>
        public void ReadWhitespace()
        {
            var c = this.Stream.PeekByte();
            while (IsInRange(c, Whitespace)) {
                this.Stream.ReadByte();
                c = this.Stream.PeekByte();
            }
        }

        /// <summary>
        /// Reads the end-of-line marker in a stream object.
        /// </summary>
        public void ReadStreamObjectEol()
        {
            // read carriage return or linefeed
            var c = this.Stream.ReadByte();
            if (c == '\r') {
                // read linefeed
                this.Stream.ReadByte();
            }
        }

        private Token? ReadToken(bool raiseExceptionOnEof)
        {
            ReadWhitespace();
            var c = this.Stream.ReadByte();
            
            // suppress comments
            if (c == '%') {
                while (c != -1 && c != '\n' && c != '\r') {
                    c = this.Stream.ReadByte();
                }
                ReadWhitespace();
                c = this.Stream.ReadByte();
            }

            // end of file
            if (c == -1) {
                if (raiseExceptionOnEof) {
                    throw new PdfParseException(Resources.UnexpectedEndOfStream);
                }
                return null;
            }

            if (c == '[') {
                return new Token(TokenType.ArrayStart);
            }
            if (c == ']') {
                return new Token(TokenType.ArrayEnd);
            }
            if (c == '<' && this.Stream.PeekByte() == '<') {
                this.Stream.ReadByte();
                return new Token(TokenType.DictionaryStart);
            }
            if (c == '>' && this.Stream.PeekByte() == '>') {
                this.Stream.ReadByte();
                return new Token(TokenType.DictionaryEnd);
            }
            if (c == '<') {
                return ReadHexString();
            }
            if (c == '(') {
                return ReadString();
            }
            if (c == '/') {
                return ReadName();
            }
            if (c == '-' || c == '+' || c == '.' || (c >= '0' && c <= '9')) {
                return ReadNumber(c);
            }
            return ReadKeyword(c);
        }

        private int ReadEscapeChar()
        {
            var c = this.Stream.PeekByte();
            if (c == '(' || c == ')' || c == '\\') {
                this.Stream.ReadByte();
                return c;
            }
            if (c == '\n') {
                this.Stream.ReadByte();
                return -1;      // ignore split strings
            }
            if (c == '\r') {
                this.Stream.ReadByte();
                if (this.Stream.PeekByte() == '\n') {
                    this.Stream.ReadByte();
                }
                return -1;      // ignore split strings
            }
            if (c == 'n') {
                this.Stream.ReadByte();
                return '\n';
            }
            if (c == 'r') {
                this.Stream.ReadByte();
                return '\r';
            }
            if (c == 't') {
                this.Stream.ReadByte();
                return '\t';
            }
            if (c == 'b') {
                this.Stream.ReadByte();
                return '\b';
            }
            if (c == 'f') {
                this.Stream.ReadByte();
                return '\f';
            }

            if (c >= '0' && c <= '7') {     // read octal number
                int c1, c2, c3;
                c1 = c;
                this.Stream.ReadByte();

                c = this.Stream.PeekByte();
                if (c >= '0' && c <= '7') {
                    c2 = this.Stream.ReadByte();
                    c = this.Stream.PeekByte();
                    if (c >= '0' && c <= '7') {
                        c3 = this.Stream.ReadByte();
                        return (c1 - '0') * 64 + (c2 - '0') * 8 + (c3 - '0');
                    }
                    else {
                        return (c1 - '0') * 8 + (c2 - '0');
                    }
                }
                else {
                    return c1 - '0';
                }
            }

            // ignore slash
            return -1;
        }

        private Token ReadString()
        {
            var result = new StringBuilder();
            var parenthesesCount = 1;
            while (true) {
                var c = this.Stream.ReadByte();
                if (c == '\\') {
                    var c2 = ReadEscapeChar();
                    if (c2 != -1) {
                        result.Append((char)c2);
                    }
                }
                else if (c == '(') {
                    result.Append('(');
                    parenthesesCount++;
                }
                else if (c == ')' && (--parenthesesCount == 0)) {
                    break;
                }
                else if (c == -1) {
                    throw new PdfParseException(Resources.UnexpectedEndOfStream);
                }
                else {
                    result.Append((char)c);
                }
            }

            return new Token(TokenType.String, result.ToString());
        }

        private Token ReadHexString()
        {
            var result = new StringBuilder();
            while (true) {
                var c = this.Stream.ReadByte();
                if (c == '>') {
                    break;
                }
                else if (c == -1) {
                    throw new PdfParseException(Resources.UnexpectedEndOfStream);
                }
                result.Append((char)c);
            }
            return new Token(TokenType.HexadecimalString, result.ToString());
        }

        private Token ReadName()
        {
            var result = new StringBuilder();
            while (true) {
                var c = this.Stream.PeekByte();
                if (IsInRange(c, Whitespace) || IsInRange(c, Delimiters) || c == -1) {
                    break;
                }
                else if (c == '#') {
                    this.Stream.ReadByte();
                    int i1 = ParserUtility.ParseHex(this.Stream.ReadByte());
                    int i2 = ParserUtility.ParseHex(this.Stream.ReadByte());

                    result.Append((char)(i1 * 16 + i2));
                }
                else {
                    result.Append((char)c);
                    this.Stream.ReadByte();
                }
            }

            return new Token(TokenType.Name, result.ToString());
        }

        private Token ReadNumber(int c)
        {
            var result = new StringBuilder();
            result.Append((char)c);

            while (true) {
                c = this.Stream.PeekByte();
                if (c == '.' || (c >= '0' && c <= '9')) {
                    result.Append((char)c);
                    this.Stream.ReadByte();
                }
                else {
                    break;
                }
            }
            return new Token(TokenType.Number, result.ToString());
        }

        private Token ReadKeyword(int c)
        {
            var result = new StringBuilder();
            result.Append((char)c);

            while (true) {
                c = this.Stream.PeekByte();
                if (IsInRange(c, Whitespace) || IsInRange(c, Delimiters) || c == -1) {
                    break;
                }
                this.Stream.ReadByte();

                result.Append((char)c);
            }
            return new Token(TokenType.Keyword, result.ToString());
        }

        private static bool IsInRange(int c, char min, char max)
        {
            return c >= min && c <= max;
        }

        private static bool IsInRange(int c, char[] chars)
        {
            for (int i = 0; i < chars.Length; i++) {
                if (c == chars[i]) {
                    return true;
                }
            }
            return false;
        }
    }
}
